延續前一天完成的會員註冊與登入功能,今天開始開發智慧醫院管理平台的預約掛號後端功能。
先前已經使用 Angular 完成預約掛號、預約成功、我的預約與預約詳細等頁面,但這些頁面主要使用固定的 Demo 資料,尚未真正將預約紀錄儲存到資料庫。
因此,今天的重點是先建立正式的預約資料結構與後端 API,讓會員未來可以依照醫師的實際門診日期進行掛號,並確保不同會員的預約資料能正確區分。
前面已經完成醫師基本資料與每週門診時間表,但在設計預約功能時,我發現「每週門診時間」和「實際可以預約的日期」是不同的。
例如:
每週門診時間主要用來呈現醫師固定的看診安排,而實際預約需要有明確的日期、時間、名額及開放狀態。
因此,這次沒有直接把預約資料寫進原本的每週門診時間表,而是新增兩張資料表。
這張資料表負責保存可以掛號的診次資訊。
| 欄位 | 用途 |
|---|---|
SCHEDULE_ID |
診次編號,主鍵 |
DOCTOR_ID |
對應醫師 |
STARTS_AT |
診次開始時間 |
ENDS_AT |
診次結束時間 |
CAPACITY |
預約名額 |
IS_BOOKABLE |
是否開放預約 |
ROOM |
診間 |
其中,SCHEDULE_ID 使用 Oracle Identity 自動產生,不需要由前端指定。
DOCTOR_ID 則透過 Foreign Key 對應既有的 DOCTORS 資料表,確保診次一定屬於資料庫中存在的醫師。
另外,也加入開始時間、結束時間與名額的檢查條件,避免建立不合理的診次。
建立正式診次後,接著需要記錄哪些會員預約了哪些診次。
因此新增 APPOINTMENTS 資料表。
| 欄位 | 用途 |
|---|---|
APPOINTMENT_ID |
預約編號,主鍵 |
PATIENT_ID |
對應病人 |
SCHEDULE_ID |
對應診次 |
APPOINTMENT_STATUS |
預約狀態 |
VISIT_REASON |
就診原因 |
REFERENCE_CODE |
顯示給會員的預約識別碼 |
CREATED_AT |
建立預約時間 |
CANCELLED_AT |
取消時間 |
目前預約狀態主要分為:
confirmed:已預約。cancelled:已取消。這次沒有在取消預約時直接刪除紀錄,而是保留原本的預約資料,將狀態更新為 cancelled,並記錄取消時間。
這樣會員未來可以查看自己的歷史預約,也方便系統追蹤預約狀態。
除了主鍵與外鍵,也建立了額外索引。
其中比較重要的是 UX_APPT_CONFIRMED。
它使用 Oracle 的條件式唯一索引,限制同一位病人在同一個診次中,不能同時存在兩筆有效預約。
也就是說:
另外,也建立查詢診次與會員預約紀錄所需的索引。
這讓我理解到,資料庫的限制條件不只是防止錯誤資料,也能成為後端驗證之外的另一層保護。
Oracle DOCTOR_SCHEDULES、APPOINTMENTS 資料表與欄位

完成資料表後,我先建立四筆虛構診次,讓後續 API 可以使用實際日期進行測試。
| 診次 ID | 日期 | 時間 | 名額 | 是否開放 |
|---|---|---|---|---|
| 1 | 2026/11/01 | 09:00~12:00 | 3 | Y |
| 2 | 2026/11/02 | 14:00~17:00 | 2 | Y |
| 3 | 2026/11/03 | 09:00~12:00 | 1 | Y |
| 4 | 2026/11/04 | 14:00~17:00 | 1 | N |
這次特別設定兩種測試情境。
第一種是正常開放的診次,可以測試預約建立與剩餘名額。
第二種是尚未開放預約的診次,即使資料庫中存在該診次,後端也應該拒絕建立預約。
另外,診次 3 的容量設定為 1,是為了後續測試兩位會員同時預約最後一個名額的情況。
完成新增後,我在 SQL Developer 查詢四筆診次,確認資料已正確儲存。
Oracle 四筆日期化測試診次】

資料庫準備完成後,開始建立預約掛號相關的 Web API。
這次沿用原本 ASP.NET Core 專案架構,將不同工作分配到 Controller、Service、Repository 與 DTO。
| HTTP | API | 功能 |
|---|---|---|
| GET | /api/schedules |
查詢指定醫師與日期的診次 |
| POST | /api/appointments |
建立預約 |
| GET | /api/appointments/me |
查詢目前會員的預約 |
| GET | /api/appointments/{id} |
查詢指定預約詳細 |
| POST | /api/appointments/{id}/cancel |
取消預約 |
這次主要新增:
SchedulesController.cs
AppointmentsController.cs
AppointmentService.cs
AppointmentRepository.cs
AppointmentDtos.cs
Controller 負責接收 HTTP 請求與回傳結果,Service 處理預約規則,而 Repository 負責與 Oracle 資料庫溝通。
我希望讓每個部分的職責清楚,避免把名額判斷、身分檢查和 SQL 全部寫在 Controller 裡面。
第一支 API 是查詢指定醫師某一天的診次。
例如:
GET /api/schedules?doctorId=1&date=2026-11-01
後端會依照醫師及日期查詢診次,並回傳名額、剩餘名額與是否可預約等資訊。
這裡有一個重要觀念:
資料表中存在診次,不代表該診次一定可以預約。
後端還需要確認診次是否開放、是否已經開始,以及是否還有名額。
建立預約使用:
POST /api/appointments
Request 範例:
{
"scheduleId": 3,
"visitReason": "虛構測試就診原因"
}
會員登入後,後端會從已驗證的登入身分取得對應的病人資料。
前端只需要提供想預約的診次及就診原因,不需要自行指定 PATIENT_ID。
這樣可以避免會員修改 Request 中的病人編號,替其他人建立預約。
建立成功時回傳 HTTP 201,並提供預約詳細資訊與 Location。
這次開發過程中,我認為比較重要的問題是:
如果同一個診次只剩下一個名額,卻有兩位會員同時送出預約,系統要怎麼避免超額掛號?
如果只有在前端顯示剩餘名額,並不能真正避免這個問題。
因為不同使用者可能在幾乎相同的時間看到「剩餘 1 名」,接著同時按下預約。
這次在後端使用 Oracle Transaction 處理預約建立。
主要流程如下:
Repository 使用 Oracle 的 SELECT ... FOR UPDATE WAIT 3 鎖定診次資料列,避免多個請求在名額檢查時互相干擾。
這次學到的是,剩餘名額不能只相信前一次查詢的結果,而是要在真正寫入前重新檢查。
Transaction 主要負責控制多個請求同時操作時的資料一致性。
條件式唯一索引則負責防止同一位病人對同一個診次保留多筆有效預約。
兩者處理的問題不同,因此需要互相搭配:
延續 Day 26 的 Cookie Authentication,預約 API 也加入會員登入驗證。
例如會員進入:
GET /api/appointments/me
後端會依登入資訊查詢對應的 PATIENT_ID,再取得該會員自己的預約紀錄。
查詢詳細資料時,也必須同時確認預約 ID 與本人身分。
如果嘗試查看其他會員的預約,後端回傳 HTTP 404,不提供該筆資料。
因此,不能只依靠 Angular 隱藏按鈕來保護資料,真正的權限檢查仍然必須由後端執行。
取消預約使用:
POST /api/appointments/{id}/cancel
這支 API 需要會員登入與 CSRF 驗證。
取消時,後端會確認預約是否屬於本人、是否符合取消條件,並透過交易更新預約狀態。
取消成功後:
APPOINTMENT_STATUS 更新為 cancelled。CANCELLED_AT。這次也加入重複取消的處理。
如果同一筆已取消預約再次收到取消請求,系統不會重新改寫第一次的取消時間。
完成 API 後,我先進行 Mock HTTP 測試,再進入真實 Oracle 環境驗證。
這次使用 Mock Repository 與交易替身,測試 Controller、Service、Cookie、CSRF,以及各種預約結果。
主要涵蓋:
| 測試 | 結果 |
|---|---|
| Day 27 Booking Mock | 286/286 通過 |
| Day 26 Auth Mock 回歸 | 304/304 通過 |
| Program 啟動回歸 | 27/27 通過 |
| 初期 Oracle GET-only | 35/35 通過 |
Mock 測試能協助確認程式邏輯,但無法完全代表真實 Oracle 的交易、列鎖與並行行為,因此還需要進一步整合驗證。
預約 API 與 Mock 測試結果

完成 Mock 測試後,我開始準備真實 Oracle 整合測試。
這次沒有直接使用正式會員資料,而是使用兩位既有的虛構測試會員,以及前面建立的測試診次。
第一次受控執行時,會員登入收到 HTTP 401,因此整個測試在建立預約之前就停止。
當時沒有新增任何預約紀錄,也沒有執行取消或 Rollback 測試。
為了避免直接重複執行造成資料不一致,我先檢查測試工具的登入流程。
過程中發現,原本使用 Console.ReadKey(true) 的密碼輸入方式,對部分編輯鍵與 Unicode 處理不夠完整。
因此,我將密碼輸入器調整為更明確的輸入與確認流程,並加入離線及 Mock 測試。
後續也建立獨立登入診斷工具,專門測試:
GET /api/auth/csrf
↓
POST /api/auth/login
↓
GET /api/auth/me
最後兩位測試會員都成功取得登入 HTTP 200,身分確認也回傳 HTTP 200。
不過,這只能證明修正後的登入流程正常,不能直接認定第一次 HTTP 401 一定是密碼輸入器造成的。
完成登入確認後,我重新建立獨立的受控測試計畫。
這次使用:
RunId:D27INT20261031B
Mode:Real Oracle / HTTP
測試開始前先確認:
整個測試也限制新增預約與取消次數,避免測試意外重複執行。
這次最重要的測試,是讓兩位會員同時預約只剩下一個名額的診次。
實際結果:
| 測試會員 | HTTP 結果 | 說明 |
|---|---|---|
syy000 |
409 | 診次已額滿 |
syy001 |
201 | 成功建立預約 |
結果符合原本設計:只有一位會員成功預約,另一位收到名額不足的錯誤。
這次真實 Oracle 測試確認,兩個並行請求沒有造成超額掛號。
不過這是受控的兩請求競爭測試,尚不代表系統已通過大量使用者的效能或壓力測試。
接著,我測試取消預約後是否能重新掛號。
實際流程:
syy001 成功建立第一筆預約。Oracle 最後保留兩筆預約歷史:
| 預約 ID | 診次 ID | 預約狀態 |
|---|---|---|
| 2 | 3 | cancelled |
| 3 | 3 | cancelled |
這代表取消預約不會直接刪除原本資料,而是保留歷史,並允許會員在取消後重新建立新的預約。
最終資料庫確認:
APPOINTMENTS:2 筆。confirmed:0 筆。cancelled:2 筆。另外,非本人查詢及取消均得到 HTTP 404,重複預約、額滿及未開放診次也依規則回傳 HTTP 409。
Oracle 兩筆 cancelled 預約紀錄

除了 HTTP API,這次也另外測試 Repository 的 Oracle Transaction Rollback。
測試方式是在交易內建立暫存預約並完成編號更新後,故意觸發失敗,確認資料庫是否能復原。
實際測試中,暫存預約使用 ID 1。
Rollback 完成後,重新查詢 Oracle,確認:
因此,這次 Repository 層的真實 Rollback 測試通過。
需要注意的是,這是資料庫交易回復測試,並不是 HTTP 網路中斷測試。
這次 Day 27 共整理了 16 項真實 Oracle 驗收項目。
| 驗收項目 | 結果 |
|---|---|
| 兩位會員登入 | 通過 |
| 未開放診次拒絕預約 | 通過 |
| 最後一席名額競爭 | 通過 |
| 成功建立預約 | 通過 |
| 額滿拒絕 | 通過 |
| 本人預約查詢 | 通過 |
| 非本人查詢與取消拒絕 | 通過 |
| 重複預約拒絕 | 通過 |
| 取消預約 | 通過 |
| 重複取消時間不變 | 通過 |
| 取消後釋出名額 | 通過 |
| 重新建立預約 | 通過 |
| 第二筆取消 | 通過 |
| 最終資料庫筆數一致 | 通過 |
| Oracle Rollback 無殘留 | 通過 |
本次 16/16 項受控驗收全部通過。
這次也讓我理解,API 測試不只是確認 HTTP 有沒有回傳成功,而是要一起檢查:
原本已經有醫師每週門診時間表,但正式預約必須對應到某一天的實際診次。
因此新增 DOCTOR_SCHEDULES,讓預約資料可以記錄日期、開始時間、結束時間、名額及開放狀態。
即使前端顯示剩餘一個名額,也可能有另一位會員同時預約。
所以真正的名額檢查需要由後端在資料庫交易中完成。
這次將取消視為狀態變更,保留原本的預約編號與取消時間。
因此會員可以有多筆歷史紀錄,但同一個診次不能同時存在重複的有效預約。
一開始 API Mock 測試全部通過,但真正進行整合驗證時,仍需要另外處理登入測試工具與 Oracle 寫入流程。
也因此,我學到要區分單元/Mock 測試與真實資料庫整合測試,不能只看測試數量就認定所有功能完成。
這次第一次測試在登入階段停止,因此保留原本執行紀錄,沒有直接重跑或刪除歷史。
後來使用新的 RunId 進行受控測試,讓每次執行都有對應的紀錄,也能確認實際建立、取消了哪些預約。
DOCTOR_SCHEDULES、APPOINTMENTS 資料表。Day 27 已完成預約掛號後端 API 與真實 Oracle 整合驗證。
接下來將繼續處理 Angular 與正式 API 的串接,包括會員個人資料、我的預約列表、預約詳細與取消預約畫面。
另外,正式「建立預約」的 Angular 頁面目前仍使用 Demo 流程,後續還需要將它串接至正式預約 API,才能讓會員直接從網站完成真正的掛號操作。
這次最大的收穫是,從原本只能展示的預約介面,逐步建立了能真正與 Oracle 資料庫互動的預約後端,也透過實際整合測試確認名額、權限及交易處理符合預期。